Koi = (function($){
    return function Koi($element, g_value, config) {

        /*********************** STRUCTURES ************************/
        /**
         * The real-line (Broken && Straight)
         * @constructor
         */
        function Line(pArr) {

            this.pArr = pArr;
            this.mouseAroundCheckers = [];
            /*TODO refactor*/
            this.options = {};

            var p1, p2;
            for (var i = 0; i < pArr.length - 1; i++) {
                p1 = pArr[i];
                p2 = pArr[i + 1];
                this.mouseAroundCheckers[i] = function (p1, p2) {
                    return function (p) {
                        //XXX use global config
                        return mouse_around_line(p.x, p.y,
                            p1, p2, CONFIG.Line.responseRange);
                    };
                }(p1, p2);
            }

        }

        Line.prototype.mouseNearby = function (mouse_p) {

            for (var i = 0; i < this.mouseAroundCheckers.length; i++) {
                if (this.mouseAroundCheckers[i](mouse_p)) return true;
            }

            return false;
        };

        Line.prototype.render = function (ctx) {

            ctx.beginPath();
            ctx.strokeStyle = this.options.strokeStyle
                || CONFIG.Edge.strokeStyle || "#444";

            if (this.pArr.length > 0) {
                ctx.moveTo(this.pArr[0].x, this.pArr[0].y);
                for (var i = 1; i < this.pArr.length; i++) {
                    ctx.lineTo(this.pArr[i].x, this.pArr[i].y);
                }
            }

            ctx.closePath();
            ctx.stroke();

            //Draw arrow -- between the lost two points C -> A
            //arrow is located at the last point.
            if (this.pArr.length >= 2 && CONFIG.G.direction == 'single') {
                draw_arrow(ctx, this.pArr[this.pArr.length - 2],
                    this.pArr[this.pArr.length - 1]);
            }

        };

        var sample_point = {
            //static
            x: 0, y: 0, options: {
                type: 'a'
            },
            //runtime
            hover:false,
            current:false
        };

        var sample_edge = {
            //static
            x: 0, y: 0,
            //index of the point in array
            from: 1,
            to: 2,
            options: {
                condition: 'true'
            },
            //runtime
            hover:false,
            current:false,
            $from: '@p1',
            $to: '@p2'
        };
        /**
         * Graphic Layer
         *
         *
         * @param g_value -- the plain json object
         * @constructor
         */
        function G(g_value) {

            //static data these two contains no more but the g-structure. TODO
            var points = this.points = g_value.points || [];
            //g_value.points ? recover_points(g_value.points) : [];
            this.edges = g_value.edges || [];
            //g_value.edges ? recover_edges(g_value.edges, this.points) : [];
            this.edges.map(function(_){edge_build_$(_,points);});

            this.props = g_value.props || [];

            /*debug */console.log(this.edges);

            this.id = g_value.id || '@uuid';

            this.name = g_value.name || 'new Process';

            this.save = function (method, url, cb) {
                //transfer this to server
                $[method](url, this, function (json) {
                    cb();
                }, 'json');
            };

            /**
             * Remove everything
             */
            this.clear = function () {
                this.edges.length = 0;
                this.points.length = 0;
            };

            //runtime data TODO
            this.add_point = function (x, y, options) {
                this.points.push({
                    x: x,
                    y: y,
                    options: options
                });
            };

            /** *
             * @param $from
             * @param $to
             * @param options
             */
            this.add_edge = function ($from, $to, options) {
                //check
                this.edges.push({
                    from: indexOf($from, this.points),
                    to: indexOf($to, this.points),
                    $from: $from,
                    $to: $to,
                    options: options || {
                        condition: true
                    }
                });
            };

            this.del_edge = function (e) {
                this.edges.splice(indexOf(e, this.edges));
            };

            this.del_point = function (p) {

                for (var i = 0; i < this.edges.length; i++) {
                    if (p == this.edges[i].$from ||
                        p == this.edges[i].$to) {
                        delete this.edges[i];
                    }
                }
                var points = this.points;
                this.edges = this.edges.filter(function(_){
                    return _ != undefined;
                }).map(function(_){
                   edge_build_idx(_, points);
                });

            };


            this.find_point = function (input) {
                var p;
                for (var i = 0; i < this.points.length; i++) {
                    p = this.points[i];
                    if(mouse_is_inside_of_point(input,p)) return p;
                }
            };

            this.find_edge = function (mouse_p) {
                var e;
                for (var i = 0; i < this.edges.length; i++) {
                    e = this.edges[i];
                    if (mouse_is_nearby_to_edge(mouse_p, e)) return e;
                }

            };

            this.find_edge_by_points = function (p_from, p_to) {
                var e;
                for (var i = 0; i < this.edges.length; i++) {
                    e = this.edges[i];
                    if (p_from == e.from && p_to == e.to) return e;
                }
            };

            this.data = function() {
                return {
                    id: this.id,
                    name: this.name,
                    /*keep arries clean*/
                    points: this.points.map(point_data),
                    edges: this.edges.map(point_data),
                    props: this.props
                }
            };

            this.move_point_to = function(point, x,y) {
                point.x = x;
                point.y = y;
            };

            this.render = function (ctx) {

                //render points
                for (var i = 0; i < this.points.length; i++) {
                    if (this.points[i]) {
                        render_point(ctx, this.points[i]);
                    }
                }

                //render edges
                for (var j = 0; j < this.edges.length; j++) {
                    if (this.edges[j]) {
                        render_edge(ctx, this.edges[j]);
                    }
                }

            };

            //TODO (functional refactor ongoing)


            /******************utils***************/


            function point_data(p) {
                return {
                    x: p.x,
                    y: p.y,
                    options: p.options
                };
            }

            function edge_data(e) {
                return {
                    x: e.x,
                    y: e.y,
                    from: e.from,
                    to: e.to,
                    options: e.options
                };
            }

            /**
             * set $from, $to as the actual references to the edge
             * @param edges
             * @param points
             */
            function edge_build_$(edge,points){
                edge.$from = points[edge.from];
                edge.$to = points[edge.to];
            }

            function edge_build_idx(edge,points){
                edge.from = indexOf(edge.$from, points);
                edge.to = indexOf(edge.$to, points);
            }

            function point_is_sync(p) {
                return p.options && p.options.type == 's';
            }

            function point_is_act(p) {
                return p.options && p.options.type == 'a';
            }

            function point_width(p) {
                return p.options.width || CONFIG.Point.width;
            }

            function point_height(p) {
                return p.options.height || CONFIG.Point.height;
            }

            function point_fill_style(p) {
                return p.options.fillStyle || CONFIG.Point.fillStyle;
            }

            function point_stroke_style(p) {
                return p.options.strokeStyle || CONFIG.Point.strokeStyle;
            }

            function point_radius(p) {
                return (min(point_height(p), point_width(p))/2)
                    || CONFIG.Point.radius;
            }

            function point_move_to(p, new_p) {
                p.x = new_p.x;
                p.y = new_p.y;
            }

            function mouse_is_inside_of_point(mouse_p, p) {
                if (point_is_act(p)) {
                    return point_on_rect(mouse_p, p,
                        point_width(p), point_height(p));
                } else if (point_is_sync(p)) {
                    return point_on_circle(mouse_p, p, point_radius(p));
                }
            }

            function mouse_is_nearby_to_edge(mouse_p, e) {
                return e.line && (e.line instanceof Line)
                    && e.line.mouseNearby(mouse_p);
            }

            function build_line_for_edge(e) {
                if (e.line && (e.line instanceof Line)) {
                    console.warn('e.line exists.')
                }
                if(e.$from !== undefined || e.$to !== undefined){
                    e.line = new Line(
                        nearest_anchors_between_points(e.$from, e.$to));
                }//TODO:use straight line for now
            }

            function build_anchors_for_point(p) {
                if (p.anchors) {
                    console.warn('p.anchors exists.');
                }
                p.anchors = {
                    n: {
                        x: p.x,
                        y: p.y - point_height(p) / 2
                    },
                    s: {
                        x: p.x,
                        y: p.y + point_height(p) / 2
                    },
                    e: {
                        x: p.x + point_width(p) / 2,
                        y: p.y
                    },
                    w: {
                        x: p.x - point_width(p) / 2,
                        y: p.y
                    }
                };
            }

            /**
             * Returns the nearest anchors
             * -- an array in which arr[0] represents the p1`s anchor and arr[1] represents the p2`s anchor.
             */
            function nearest_anchors_between_points($from, $to) {
                var ret = null;
                var from_a = $from.anchors;
                var to_a = $to.anchors;
                do_on_quadrant(quadrant($from, $to),
                    function () {
                        ret = determineShortestLine([
                            {p1: from_a.n, p2: to_a.w},
                            {p1: from_a.n, p2: to_a.s},
                            {p1: from_a.e, p2: to_a.w},
                            {p1: from_a.e, p2: to_a.s}
                        ]);
                    },
                    function () {
                        ret = determineShortestLine([
                            {p1: from_a.s, p2: to_a.w},
                            {p1: from_a.s, p2: to_a.n},
                            {p1: from_a.e, p2: to_a.w},
                            {p1: from_a.e, p2: to_a.n}
                        ]);
                    },
                    function () {
                        ret = determineShortestLine([
                            {p1: from_a.s, p2: to_a.e},
                            {p1: from_a.s, p2: to_a.n},
                            {p1: from_a.w, p2: to_a.e},
                            {p1: from_a.w, p2: to_a.n}
                        ]);
                    },
                    function () {
                        ret = determineShortestLine([
                            {p1: from_a.n, p2: to_a.e},
                            {p1: from_a.n, p2: to_a.s},
                            {p1: from_a.w, p2: to_a.e},
                            {p1: from_a.w, p2: to_a.s}
                        ]);
                    }
                );
                return ret;
            }

            function render_point(ctx, point) {

                //anchors
                build_anchors_for_point(point);

                if (point_is_act(point)) {
                    ctx.strokeStyle = point_stroke_style(point);
                    ctx.fillStyle = point_fill_style(point);
                    var h = point_height(point);
                    var w = point_width(point);
                    ctx.strokeRect(
                        point.x - w / 2,
                        point.y - h / 2,
                        w, h);
                    //render name
                    ctx.font = "13px Verdana";
                    ctx.textAlign = 'center';
                    ctx.fillText(point.options.name,
                                 point.x,
                                 point.y - h / 4);
                } else if (point_is_sync(point)) {
                    var r = point_radius(point);
                    ctx.strokeStyle = point_stroke_style(point);
                    ctx.beginPath();
                    ctx.arc(point.x, point.y, r, Math.PI * 2, false);
                    ctx.closePath();
                    ctx.stroke();
                } else {
                    throw 'unfinished';
                }

                /*TODO config*/
                if (CONFIG.renderAnchors)
                    point.renderAnchors(ctx, point.anchors());
            }

            function render_edge(ctx, edge) {
                build_line_for_edge(edge);
                ///*debug TODO*/ console.log('build edge',edge);
                //edge.line
                if(edge.line)
                    edge.line.render(ctx);
            }

        }

        /*
         * Initialization
         */
        if (!$element) throw new Error('element must not be null');

        var CONFIG = $.extend({
            strokeStyle: '#444',
            fillStyle: '#444',
            hoverStyle: 'green',
            currentStyle: 'red',
            renderAnchors: false,
            logLevel: 3, /*0 1 for warn, 2 for*/
            G: {
                direction: 'single'
            },
            Line: {
                responseRange: 2
            },
            Point: {
                strokeStyle: '#444',
                fillStyle:'#444',
                hoverStyle:'#006699',
                currentStyle:'red',
                width:100,
                height:80,
                radius:80
            },
            Edge: {
                currentStyle:'red',
                hoverStyle:'#006699',
                strokeStyle: '#555',
                fillStyle:'#555'

            }
        }, config);

        /*** Exports
         *  export CamelStyle
         *  ****/
        this.g = new G(g_value);
        // the canvas
        this.$canvas = $element;

        this.ctx = $element.get(0).getContext('2d');

        //current point or edge
        this.current = undefined;
        //System

        /**
         * Open from url
         * @param json
         */
            //TODO
        this.open = function (json) {
        };

        /**
         *
         */
            //TODO
        this.save = function () {
        };

        this.delPoint = function (x, y) {
            var p = {x: x, y: y};
            var point = this.g.find_point(p);
            if (point) {
                this.g.del_point(point);
            }
        };

        this.createPoint = function (x, y, option) {
            //this.g.addPoint(new Point(x, y, option));
            this.g.add_point(x, y, option);
        };

        this.createEdge = function ($from, $to, option) {
            //assert p1 is a valid point
            this.g.add_edge($from, $to, option);
        };

        this.delEdge = function (p) {
            var e = this.g.find_edge(p);
            if (e) this.g.del_edge(e);
        };

        this.findPoint = function (p) {
            return this.g.find_point(p);
        };


        this.select = function (x, y) {
            var mouse = {x: x, y: y};
            this.setCurrent(
                this.g.find_edge(mouse) || this.g.find_point(mouse));
        };

        /*TODO rename or delete this*/ this.hover = style;

        /*TODO rename or delete this*/ this.clearHover = clearStyle;

        this.deleteCurrent = function () {
            if (this.current) {
                if (element_is_edge(this.current))
                    this.g.del_edge(this.current);
                else if(element_is_point(this.current))
                    this.g.del_point(this.current);
                this.clearCurrent();
            }
        };

        this.clearCurrent = function() {
            if (this.current) {
                this.current.strokeStyle = CONFIG.strokeStyle;
                this.current = undefined;
            }
        };

        this.setCurrent = function(o) {
            console.log('setCurrent_input', o);
            if (o) {
                this.clearCurrent();
                this.current = o;
                o.strokeStyle = CONFIG.currentStyle;
            }
        };

        this.render = function () {
            _empty_canvas(this.ctx, this.$canvas);
            this.g.render(this.ctx);
        };

        this.prop = function (k, v) {
            if (k && v) {
                this.g.props[k] = v;
            }
            else if (k) {
                return this.g.props[k];
            }
        };


        //private functions

        function element_is_edge(e) {
            return e.from !== undefined && e.to !== undefined;
        }

        function element_is_point(e) {
            return !element_is_edge(e);
        }


        function style(o, style) {
            if (o && o !== this.current)
                o.strokeStyle = style || CONFIG.strokeStyle;
        }

        function clearStyle(o) {
            if (o) {
                if (o !== this.current)
                    o.strokeStyle = CONFIG.strokeStyle;
            }
            //clear all
            else {
                for (var i = 0; i < this.g.points.length; i++) {
                    clearStyle(this.g.points[i]);
                }
                for (var j = 0; j < this.g.edges.length; j++) {
                    clearStyle(this.g.edges[j]);
                }
            }
        }

        //function setCurrent(o) {
        //}

        //function clearCurrent() {
        //}

        function _empty_canvas(ctx, $canvas) {
            ctx.clearRect(0, 0, $canvas.width(), $canvas.height());
        }


        /*
         * Utils
         */

        function drawLine(ctx, p1, p2, style) {
            ctx.beginPath();
            ctx.moveTo(p1.x, p1.y);
            ctx.lineTo(p2.x, p2.y);
            ctx.closePath();
            ctx.stroke();
        }

        function indexOf(obj, arr) {
            for (var i = 0; i < arr.length; i++) {
                if (arr[i] === obj) return i;
            }
            return -1;
        }

        function deleteInArray(obj, arr) {
            var idx = indexOf(obj, arr);
            if (idx !== -1) {
                arr.splice(idx);
            }
        }

        function determineShortestLine(lineArr) {
            var min_id = -1, min, d, l;
            for (var i = 0; i < lineArr.length; i++) {
                l = lineArr[i];
                d = distance(l.p1, l.p2);
                if (min === undefined) {
                    min = d;
                    min_id = i;
                }
                else {
                    if (d < min) {
                        min = d;
                        min_id = i;
                    }
                }
            }
            //return {
            //    p1: lineArr[min_id].p1,
            //    p2: lineArr[min_id].p2,
            //    d: min
            //}
            return [lineArr[min_id].p1, lineArr[min_id].p2];
        }

        function distance(p1, p2) {
            return Math.sqrt((p1.x - p2.x) * (p1.x - p2.x)
                            + (p1.y - p1.y) * (p1.y - p1.y));
        }

        function min(a, b) {
            return a < b ? a : b;
        }

        function mouse_around_line(mouseX, mouseY, p1, p2, r) {
            var theta = Math.atan((p1.x - p2.x) / (p2.y - p2.y));
            var d_x = 0.5 * (p1.x + p2.x);
            var d_y = 0.5 * (p1.y + p2.y);
            var x = (mouseX - d_x) / Math.sin(theta);
            var y = (mouseY - d_y) / Math.cos(theta);
            var A = 0.5 * distance(p1, p2);
            if (y > A + r || y < 0 - A - r) return false;
            if (y >= A && y <= A + r) {
                return Math.pow(x, 2) + Math.pow((y - A), 2) <= Math.pow(r, 2);
            }
            if (y > 0 - A && y < A) {
                return x > 0 - r && x < r;
            }
            if (y >= 0 - A - r && y <= 0 - A) {
                return Math.pow(x, 2) + Math.pow((y + A), 2) <= Math.pow(r, 2);
            }
            return false;
        }

        function draw_arrow(ctx, from, to) {

            var o = to;
            var a = from;
            var r = 30;
            var theta = 15 * Math.PI / 180;


            // get the p2`s nearest point( n,e,s,w )
            var delta_x = o.x - a.x,
            //x,y starts from the top-left corner
                delta_y = 0 - (o.y - a.y);

            //first,
            // we let the form point at the zero-point then value the relative position
            var quadrant = (function () {
                if (delta_x > 0) {
                    return delta_y > 0 ? 1 : 2;
                } else {
                    return delta_y > 0 ? 4 : 3;
                }
            })();

            var bd = (function () {
                var gama;
                switch (quadrant) {
                    case 1:
                    {
                        gama = Math.atan((a.y - o.y) / (a.x - o.x));
                        return {
                            b: {
                                x: o.x - Math.cos(gama + theta) * r,
                                y: o.y - Math.sin(gama + theta) * r
                            },
                            d: {
                                x: o.x - Math.cos(gama - theta) * r,
                                y: o.y - Math.sin(gama - theta) * r
                            }
                        };
                    }
                    case 2:
                        gama = Math.atan((a.y - o.y) / (o.x - a.x));
                        return {
                            b: {
                                x: o.x - Math.cos(gama + theta) * r,
                                y: o.y + Math.sin(gama + theta) * r
                            },
                            d: {
                                x: o.x - Math.cos(gama - theta) * r,
                                y: o.y + Math.sin(gama - theta) * r
                            }
                        };
                    case 3:
                        gama = Math.atan((o.y - a.y) / (a.x - o.x));
                        return {
                            b: {
                                x: Math.cos(gama + theta) * r + o.x,
                                y: o.y - Math.sin(gama + theta) * r
                            },
                            d: {
                                x: Math.cos(gama - theta) * r + o.x,
                                y: o.y - Math.sin(gama - theta) * r
                            }
                        };
                    case 4:
                        gama = Math.atan((o.y - a.y) / (o.x - a.x));
                        return {
                            b: {
                                x: Math.cos(gama + theta) * r + o.x,
                                y: Math.sin(gama + theta) * r + o.y
                            },
                            d: {
                                x: Math.cos(gama - theta) * r + o.x,
                                y: Math.sin(gama - theta) * r + o.y
                            }
                        };
                }
            })(
            );

            //draw line:AB
            ctx.beginPath();
            ctx.moveTo(bd.b.x, bd.b.y);
            ctx.lineTo(o.x, o.y);
            ctx.lineTo(bd.d.x, bd.d.y);
            ctx.closePath();
            ctx.fillStyle = ctx.strokeStyle;
            ctx.fill();
        }

        /**
         * get center point
         */
        function center_point(p) {
            return {x: p.x, y: p.y};
        }

        function point_on_rect(p, o, w, h) {
            return (
                p.x < (o.x + w / 2) && p.x > (o.x - w / 2)
                &&
                p.y < (o.y + h / 2) && p.y > (o.y - h / 2)
            );
        }

        function point_on_circle(p, o, r) {
            return (
                Math.pow(p.x - o.x, 2) + Math.pow(p.y - o.y, 2)
                <= Math.pow(r, 2)
            );
        }

        function do_on_quadrant(qua, cb1, cb2, cb3, cb4) {
            switch (qua) {
                case 1 :
                    cb1();
                    break;
                case 2 :
                    cb2();
                    break;
                case 3 :
                    cb3();
                    break;
                case 4 :
                    cb4();
                    break;
            }
        }

        /**
         * from -> to (use from as the zero-point)
         * (we assume the (0,0) is the LEFT-TOP corner, that is, delta-y is always negative against the common sense)
         */
        function quadrant(from, to) {
            // get the p2`s nearest point( n,e,s,w )
            var delta_x = to.x - from.x,
            //x,y starts from the top-left corner
                delta_y = 0 - (to.y - from.y);

            if (delta_x > 0) {
                return delta_y > 0 ? 1 : 2;
            } else {
                return delta_y > 0 ? 4 : 3;
            }

        }

    }
})($);
